package pokerserver.core;

import pokerserver.core.network.ServerListener;
import pokerserver.core.network.ServerThread;
import common.Card;
import common.GameAction;
import common.GameState;
import common.Message;
import common.TrunkatedPlayer;

/**
 * The ServerModel. This class contains most of the logic which the server
 * handles. Including, but not exclusively, logic to create a new deck, add new
 * players, deal cards, etc. It also handles incoming actions, keeps tracks of
 * players, changes turns, announces the winner, and more!
 * 
 */
public class ServerModel implements IServer {

	/**
	 * Class variables
	 */
	private static ServerModel singletonServerModel;
	private static final int port = 5000; // The port on which to listen to.
	private Player[] playerArray; // A list of players in the game.
	private Deck deck;
	private int turnCounter; // A counter to keep track of who's turn it is.
	private int pot;
	private boolean hasStarted; // Indicates if the game has started or not.
	private Card[] communityCards; // The cards on the "table".

	private static ServerListener listener = null;

	private Player latestBetter = null; // The last player to place a bet.
	private int latestBetterPosition = -1; // The position of that last better.
	// -1 = none.

	private int movesSinceBet = 0; // Reflects number of "moves" since the last
	// bet.

	private boolean betToMatchExists = false; // Keeps track if there has been a

	// bet for the player to match
	// or not.

	/**
	 * Creates a new ServerModel instance, and at the same time initiates the
	 * ServerListener, as well as starting it.
	 */
	private ServerModel() {
		playerArray = new Player[4];
	}

	public static void startNetwork() {
		if (listener == null) {
			listener = new ServerListener(port);
			listener.start();
		}
	}

	@Override
	public void addPlayer(Player player) throws IndexOutOfBoundsException {
		for (int i = 0; i < 4; i++) {
			if (playerArray[i] == null && !hasStarted) {
				playerArray[i] = player;
				playerArray[i].setPosition(i);
				return;
			}
		}
		throw new IndexOutOfBoundsException("You cannae get in!");
	}

	@Override
	public void newDeck() {
		deck = new Deck();
		deck.shuffle();
	}

	/**
	 * Singleton getter.
	 * 
	 * @return The ServerModel instance.
	 */
	public static synchronized ServerModel getInstance() {
		if (singletonServerModel == null) {
			singletonServerModel = new ServerModel();
		}
		return singletonServerModel;
	}

	@Override
	public Message getGameStateMessage(ServerThread client) {
		Player player = getPlayer(client);
		Card[] hand = player.getHand().getPlayerCards();

		TrunkatedPlayer[] newPlayerArray = new TrunkatedPlayer[4];
		
		for (int i = 0; i < 4; i++) {
			Player p = playerArray[i];
			if (p != null) {
				newPlayerArray[i] = playerArray[i].generateTrunkPlayer();
			}
		}
		
		GameState gameState = new GameState(turnCounter, newPlayerArray, pot,
				hasStarted, communityCards, hand, player.getPosition(),
				betToMatchExists);
		return new Message("Server", 3, gameState);
	}

	@Override
	public void announceWinner() {
		// Creates a new list with players that haven't folded or lost, the
		// player that have folded will be "null". this list will be sent to the
		// HandComparator
		Player[] lastManStandingPlayerList = new Player[4];
		for (int i = 0; i < 4; i++) {
			Player p = playerArray[i];
			if (p != null) {
				if (p.isFolded() || p.getAction() == GameAction.GAME_OVER)
					lastManStandingPlayerList[i] = null;
				else
					lastManStandingPlayerList[i] = p;
			}
		}

		Player[] winners = HandComparator.determineWinner(
				lastManStandingPlayerList, communityCards);

		handOutPot(winners);
	}

	@Override
	public void handOutPot(Player[] winners) {
		int winnerCounter = 0;
		for (int i = 0; i < 4; i++) {
			Player p = winners[i];
			if (p != null) {
				System.out.println(p + " won!");
				p.setAction(GameAction.WIN);
				winnerCounter++;
			} else {
				if (playerArray[i] != null) {
					playerArray[i].setAction(GameAction.LOSE);
					if (playerArray[i].getStack() == 0) {
						playerArray[i].setFolded(true);
						playerArray[i].setAction(GameAction.GAME_OVER);
					}
				}
			}
		}

		int split = pot / winnerCounter; // split the pot (splits with 1 if
		// there is only one winner).

		// Deal out the pot to the winner(s).
		for (Player p : winners) {
			if (p != null) {
				p.setStack(p.getStack() + split);
			}
		}
	}

	@Override
	public void removePlayer(ServerThread client) {
		Player player = getPlayer(client);
		player.setAction(GameAction.GAME_OVER);
	}

	/**
	 * 
	 * @param client
	 *            The ServerThread associated with the player's position you
	 *            want to get.
	 * @return The position of the player associated with the ServerThread.
	 */
	public int getPosition(ServerThread client) {
		// finding out which player made the call shouldn't be harder than
		// finding out which client the ServerThread belongs to.
		Player p = getPlayer(client);
		return p.getPosition();
	}

	@Override
	public void setNickname(ServerThread client, String nickname) {
		getPlayer(client).setNickName(nickname);
	}

	/**
	 * A method which finds the player that a given ServerThread instance
	 * belongs to.
	 * 
	 * @param client
	 *            The ServerThread associated with the player you want to find.
	 * @return The corresponding player.
	 */
	private Player getPlayer(ServerThread client) {
		for (Player p : playerArray) {
			if (p != null) {
				if (p.getPlayerThread().equals(client)) {
					return p; // found the right player, returning it.
				}
			}
		}
		return null; // no right player ever got found, returning null.
	}

	@Override
	public void doAction(ServerThread client, GameAction action) {
		// Make sure that the player is allowed to perform whatever action it's
		// trying to:
		Player player = getPlayer(client);
		if ((player.getPosition() != turnCounter) || (player.isFolded())
				|| player.getAction() == GameAction.GAME_OVER) {
			return;
		}

		if (action == GameAction.FOLD) {
			fold(player);
		} else if (action == GameAction.CHECK) {
			check(player);
		} else if (action == GameAction.CALL) {
			call(player);
		} else if (action == GameAction.ALL_IN) {
			allIn(player);
		}
	}

	/**
	 * A method which is called if a player wishes to bet.
	 * 
	 * @param client
	 *            the ServerThread of the client who wishes to bet.
	 * @param action
	 *            The action, in this method, always BET.
	 * @param bet
	 *            The amount the client wishes to bet.
	 */
	public void doAction(ServerThread client, GameAction action, int bet) {
		// First, check to see if this call on this method shouldn't be allowed:
		// the player shouldn't be folded or have lost the game, and the player
		// should have enough
		// money to perform the bet.
		Player player = getPlayer(client);
		if (player.isFolded() || player.getAction() == GameAction.GAME_OVER
				|| player.getStack() <= bet) {
			// Refuse to do anything!
			return;
		}
		bet(player, bet);
	}

	@Override
	public boolean startGame(ServerThread client) {
		// Perform a quick check to make sure the player who has started the
		// game is allowed to start, and that we have at least two players
		// (since one-man-poker is about the most boring thing there is on the
		// planet).
		Player player = getPlayer(client);
		if (!(player.getPosition() == 0)) {
			return false;
		} else {
			// Make sure more than one player is connected
			int numberOfPlayers = Utils.elementsInArray(playerArray);
			if (numberOfPlayers > 1) {
				hasStarted = true;
				newRound();
				return true;
			} else {
				return false;
			}
		}
	}

	@Override
	public void check(Player player) {
		if (betToMatchExists) {
			System.out.println("You cannot check. Someone has betted!");
			return;
		}
		player.setAction(GameAction.CHECK);
		nextPlayer();
	}

	@Override
	public void fold(Player player) {
		player.setFolded(true);
		player.setAction(GameAction.FOLD);

		int i = Utils.elementsInArray(playerArray);
		int folded = 0;
		for (Player p : playerArray) {
			if (p != null) {
				if (p.isFolded() || p.getAction() == GameAction.GAME_OVER)
					folded++;
				if (folded == i - 1) {
					finishGameEarly();
					return;
				}
			}
		}
		nextPlayer();
	}

	@Override
	public void allIn(Player player) {
		int bet = player.getStack();
		pot = pot + bet;
		player.increaseAmountInPot(bet);
		player.setAction(GameAction.ALL_IN);
		latestBetter = player;
		betToMatchExists = true;
		movesSinceBet = 0;
		nextPlayer();
	}

	@Override
	public void call(Player player) {
		if (!betToMatchExists) {
			System.out.println("You cannot CALL. Noone has betted!");
			return;
		}
		int bet = latestBetter.getAmountInPot() - player.getAmountInPot();
		if ((player.getStack() < bet)) {
			// If the player has less money than the bet has to be, go all-in.
			allIn(player);
		} else if (player.getStack() >= bet) {
			pot = pot + bet;
			player.increaseAmountInPot(bet);
			player.setAction(GameAction.CALL);
		}
		nextPlayer();
	}

	@Override
	public void bet(Player player, int bet) {
		// In case this turns out to be a "raise": make sure enough money is
		// available to the player.
		if (latestBetter != null) {
			if (!(player.getStack() >= (latestBetter.getAmountInPot()
					- player.getAmountInPot() + bet))) {
				// The player does not have the cash to follow through with the
				// raise - leave the method.
				return;
			} else {
				int newBet = bet + latestBetter.getAmountInPot()
						- player.getAmountInPot();
				player.increaseAmountInPot(newBet);
				pot = pot + newBet;
			}
		} else {
			player.increaseAmountInPot(bet);
			pot = pot + bet;
		}
		player.setAction(GameAction.BET);
		latestBetter = player;
		betToMatchExists = true;
		movesSinceBet = 0;
		nextPlayer();
	}

	@Override
	public void advanceGame() {
		// Reset turnvalues.
		movesSinceBet = 0;
		betToMatchExists = false;
		latestBetter = null;
		latestBetterPosition = -1;

		// set the turncounter to the first player in order 1-4 that haven't
		// folded or lost.
		for (int i = 0; i < 4; i++) {
			if (!(playerArray[i].isFolded())) {
				if (!(playerArray[i].getAction() == GameAction.GAME_OVER))
					turnCounter = i;
				break;
			}

		}

		if (communityCards != null) {
			/*
			 * Count how many cards we currently have in the extended flop (the
			 * array with all 5 community cards). This tells us what stage the
			 * gameplay is in.
			 */
			int numberOfCards = Utils.elementsInArray(communityCards);
			if (numberOfCards == 0) {
				// draw 3 community cards ("the flop"):
				communityCards[0] = deck.drawCard();
				communityCards[1] = deck.drawCard();
				communityCards[2] = deck.drawCard();
			} else if (numberOfCards == 3) {
				// draw 1 community card ("the turn"):
				communityCards[3] = deck.drawCard();
			} else if (numberOfCards == 4) {
				// draw 1 community card ("the river"):
				communityCards[4] = deck.drawCard();
			} else if (numberOfCards == 5) {
				// Finish the round!
				announceWinner();
				newRound();
			}
		}
	}

	@Override
	public void newRound() {

		pot = 0;
		movesSinceBet = 0;
		communityCards = new Card[5];
		newDeck();
		latestBetter = null;
		latestBetterPosition = -1;

		betToMatchExists = false;

		turnCounter = 0;
		// Set the turncounter to the right player.
		for (int i = 0; i < 4; i++) {
			Player p = playerArray[i];
			if (p != null) {
				if (p.getAction() != GameAction.GAME_OVER) {
					turnCounter = p.getPosition();
					break;
				}
			}
		}

		// Reset the amounts in pot to 0, and reset folded players for the new
		// round.
		for (int i = 0; i < 4; i++) {
			Player p = playerArray[i];
			if (p != null) {
				p.setAmountInPot(0);
				if (p.getAction() == GameAction.GAME_OVER) {
					// Set the hand to empty cards.
					Card c1 = new Card();
					Card c2 = new Card();
					Hand h = new Hand(c1, c2);
					p.setHand(h);
				} else {
					p.setHand(new Hand(deck));
					if (p.isFolded())
						p.setFolded(false);
				}
			}
		}
	}

	/**
	 * If the game is finished early, which can happen if all other players have
	 * folded, this method should be called. It finds out who's the winner, and
	 * deals out the pot accordingly, as well as starting a new round.
	 */
	private void finishGameEarly() {
		Player[] winner = new Player[4];
		for (Player p : playerArray)
			if (p != null) {
				if (!p.isFolded() && !(p.getAction() == GameAction.GAME_OVER)) {
					winner[p.getPosition()] = p;
				}
				p.setFolded(false);
			}
		handOutPot(winner);
		newRound();
	}

	@Override
	public void nextPlayer() {
		// Go to the next player in turn:
		Player nextPlayer = null;

		do {
			turnCounter = nextPlayerPosition(turnCounter);
			movesSinceBet++;
			nextPlayer = playerArray[turnCounter];

		} while (nextPlayer == null
				|| nextPlayer.getAction() == GameAction.FOLD
				|| nextPlayer.getAction() == GameAction.GAME_OVER);

		// Check if betToMatchExists should be set to false:
		if (latestBetter == null) {
			betToMatchExists = false;
		} else if (latestBetterPosition == turnCounter) {
			betToMatchExists = false;
		}
		// We need to make sure that everyone gets to make a move.
		if (movesSinceBet > 3) {
			// We set the above to 3 because it counts "empty" spots as well
			if (latestBetterPosition == nextPlayerPosition(turnCounter)) {
				advanceGame();
			} else if (latestBetterPosition == -1) {
				// No bet has yet occurred in this game
				advanceGame();
			}
		}
	}

	/**
	 * Takes in the current turn, and calculates who is next in turn.
	 * 
	 * @param turnCounter
	 *            the current countervalue.
	 * @return the next value
	 */
	private int nextPlayerPosition(int turnCounter) {
		turnCounter++;
		if (turnCounter > 3) {
			turnCounter = 0;
		}
		return turnCounter;
	}
}
